/*-------------------------------------------------------------------------
 *
 * domains.c
 *	  域类型的 I/O 函数。
 *
 * 域类型的输出函数与其基础基本类型提供的相同。输入函数必须准备应用通过类型定义的任何约束。因此，我们创建特殊的输入函数，调用基本类型的输入函数，然后检查约束。
 *
 * 进行约束检查所需的开销可能很高，因为检查目录以发现给定域的约束并不便宜。我们有三种机制来最小化此成本：
 *	1.  我们依靠 typcache 保持约束的最新副本。
 *	2.  在一个域嵌套中，我们将所有级别的检查压平为一次操作（typcache 为我们执行此操作）。
 *	3.  如果有 CHECK 约束，我们缓存一个独立的 ExprContext 来评估它们。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/utils/adt/domains.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/htup_details.h"
#include "catalog/pg_type.h"
#include "executor/executor.h"
#include "lib/stringinfo.h"
#include "utils/builtins.h"
#include "utils/expandeddatum.h"
#include "utils/lsyscache.h"
#include "utils/syscache.h"
#include "utils/typcache.h"


/*
 * 用于跨多次调用缓存状态的结构
 */
typedef struct DomainIOData
{
	Oid			domain_type;
	/* 调用基本类型输入函数所需的数据 */
	Oid			typiofunc;
	Oid			typioparam;
	int32		typtypmod;
	FmgrInfo	proc;
	/* 对要检查的约束项的缓存列表的引用 */
	DomainConstraintRef constraint_ref;
	/* 用于评估 CHECK 约束的上下文 */
	ExprContext *econtext;
	/* 此缓存所在的内存上下文 */
	MemoryContext mcxt;
} DomainIOData;


/*
 * domain_state_setup - 为新域类型初始化缓存。
 *
 * 注意：我们不能为新域类型重用相同的缓存结构，
 * 因为没有释放 DomainConstraintRef 的规定。
 * 如果调用位置需要处理新域类型，我们就在查询期间
 * 泄漏旧的结构。
 */
static DomainIOData *
fc_domain_state_setup(Oid fc_domainType, bool fc_binary, MemoryContext fc_mcxt)
{
	DomainIOData *fc_my_extra;
	TypeCacheEntry *fc_typentry;
	Oid			fc_baseType;

	fc_my_extra = (DomainIOData *) MemoryContextAlloc(fc_mcxt, sizeof(DomainIOData));

	/*
	 * 验证 domainType 表示有效的域类型。我们需要在这里
	 * 小心，因为 domain_in 和 domain_recv 可能会从 SQL
	 * 被调用，参数可能不正确。我们主要使用 lookup_type_cache
	 * 是因为它会对坏 OID 抛出一个干净的用户友好错误；
	 * 但它也可以缓存底层基本类型信息。
	 */
	fc_typentry = lookup_type_cache(fc_domainType, TYPECACHE_DOMAIN_BASE_INFO);
	if (fc_typentry->typtype != TYPTYPE_DOMAIN)
		ereport(ERROR,
				(errcode(ERRCODE_DATATYPE_MISMATCH),
				 errmsg("type %s is not a domain",
						format_type_be(fc_domainType))));

	/* 找到基本类型 */
	fc_baseType = fc_typentry->domainBaseType;
	fc_my_extra->typtypmod = fc_typentry->domainBaseTypmod;

	/* 查找底层 I/O 函数 */
	if (fc_binary)
		getTypeBinaryInputInfo(fc_baseType,
							   &fc_my_extra->typiofunc,
							   &fc_my_extra->typioparam);
	else
		getTypeInputInfo(fc_baseType,
						 &fc_my_extra->typiofunc,
						 &fc_my_extra->typioparam);
	fmgr_info_cxt(fc_my_extra->typiofunc, &fc_my_extra->proc, fc_mcxt);

	/* 查找域的约束 */
	InitDomainConstraintRef(fc_domainType, &fc_my_extra->constraint_ref, fc_mcxt, true);

	/* 我们在需要时才创建 ExprContext */
	fc_my_extra->econtext = NULL;
	fc_my_extra->mcxt = fc_mcxt;

	/* 标记缓存为有效 */
	fc_my_extra->domain_type = fc_domainType;

	return fc_my_extra;
}

/*
 * domain_check_input - 应用缓存的检查。
 *
 * 这大致类似于在 execExpr*.c 中处理 CoerceToDomain 节点，
 * 但我们分别执行每个约束，而不是将它们编译到
 * 较大表达式中。
 */
static void fc_domain_check_input(Datum fc_value, bool fc_isnull, DomainIOData *fc_my_extra)
{
	ExprContext *fc_econtext = fc_my_extra->econtext;
	ListCell   *fc_l;

	/* 确保我们拥有最新的约束 */
	UpdateDomainConstraintRef(&fc_my_extra->constraint_ref);

	foreach(fc_l, fc_my_extra->constraint_ref.constraints)
	{
		DomainConstraintState *fc_con = (DomainConstraintState *) lfirst(fc_l);

		switch (fc_con->constrainttype)
		{
			case DOM_CONSTRAINT_NOTNULL:
				if (fc_isnull)
					ereport(ERROR,
							(errcode(ERRCODE_NOT_NULL_VIOLATION),
							 errmsg("domain %s does not allow null values",
									format_type_be(fc_my_extra->domain_type)),
							 errdatatype(fc_my_extra->domain_type)));
				break;
			case DOM_CONSTRAINT_CHECK:
				{
					/* 如果尚未创建，则创建 econtext */
					if (fc_econtext == NULL)
					{
						MemoryContext fc_oldcontext;

						fc_oldcontext = MemoryContextSwitchTo(fc_my_extra->mcxt);
						fc_econtext = CreateStandaloneExprContext();
						MemoryContextSwitchTo(fc_oldcontext);
						fc_my_extra->econtext = fc_econtext;
					}

					/*
					 * 设置由 CoerceToDomainValue
					 * 节点返回的值。与通用表达式的情况不同，
					 * 这个 econtext 不能与其他任何东西共享，
					 * 所以不需要保存和恢复字段。但我们确实需要
					 * 保护传入的值不被调用的函数更改。
					 * （对于大多数用法，它不能是 R/W 扩展
					 * 对象，但对于 domain_check()，这似乎是可能的。）
					 */
					fc_econtext->domainValue_datum =
						MakeExpandedObjectReadOnly(fc_value, fc_isnull,
												   fc_my_extra->constraint_ref.tcache->typlen);
					fc_econtext->domainValue_isNull = fc_isnull;

					if (!ExecCheck(fc_con->check_exprstate, fc_econtext))
						ereport(ERROR,
								(errcode(ERRCODE_CHECK_VIOLATION),
								 errmsg("value for domain %s violates check constraint \"%s\"",
										format_type_be(fc_my_extra->domain_type),
										fc_con->name),
								 errdomainconstraint(fc_my_extra->domain_type,
													 fc_con->name)));
					break;
				}
			default:
				elog(ERROR, "unrecognized constraint type: %d",
					 (int) fc_con->constrainttype);
				break;
		}
	}

	/*
	 * 在退出之前，调用任何关闭回调并重置 econtext 的
	 * 每元组内存。如果表达式中有任何内容拥有非内存资源，
	 * 这可以避免泄漏。
	 */
	if (fc_econtext)
		ReScanExprContext(fc_econtext);
}


/*
 * domain_in - 任何域类型的输入例程。
 */
Datum domain_in(PG_FUNCTION_ARGS)
{
	char	   *fc_string;
	Oid			fc_domainType;
	DomainIOData *fc_my_extra;
	Datum		fc_value;

	/*
	 * 由于 domain_in 不是严格的，我们必须检查空输入。
	 * typioparam 参数在正常系统使用中永远不应该为空，但在手动调用中可能为空——如果是这样，只需返回空。
	 */
	if (PG_ARGISNULL(0))
		fc_string = NULL;
	else
		fc_string = PG_GETARG_CSTRING(0);
	if (PG_ARGISNULL(1))
		PG_RETURN_NULL();
	fc_domainType = PG_GETARG_OID(1);

	/*
	 * 我们安排在一系列调用中仅查找所需的信息一次，
	 * 假设域类型在我们面前不会变化（这实际上不应该发生，但如果发生，就处理它）。
	 */
	fc_my_extra = (DomainIOData *) fcinfo->flinfo->fn_extra;
	if (fc_my_extra == NULL || fc_my_extra->domain_type != fc_domainType)
	{
		fc_my_extra = fc_domain_state_setup(fc_domainType, false,
									  fcinfo->flinfo->fn_mcxt);
		fcinfo->flinfo->fn_extra = (void *) fc_my_extra;
	}

	/*
	 * 调用基础类型的 typinput 过程来转换数据。
	 */
	fc_value = InputFunctionCall(&fc_my_extra->proc,
							  fc_string,
							  fc_my_extra->typioparam,
							  fc_my_extra->typtypmod);

	/*
	 * 进行必要的检查以确保它是一个有效的域值。
	 */
	fc_domain_check_input(fc_value, (fc_string == NULL), fc_my_extra);

	if (fc_string == NULL)
		PG_RETURN_NULL();
	else
		PG_RETURN_DATUM(fc_value);
}

/*
 * domain_recv - 任何域类型的二进制输入例程。
 */
Datum domain_recv(PG_FUNCTION_ARGS)
{
	StringInfo	fc_buf;
	Oid			fc_domainType;
	DomainIOData *fc_my_extra;
	Datum		fc_value;

	/*
	 * 由于 domain_recv 不是严格的，我们必须检查空输入。
	 * typioparam 参数在正常系统使用中永远不应该为空，但在手动调用中可能为空——如果是这样，只需返回空。
	 */
	if (PG_ARGISNULL(0))
		fc_buf = NULL;
	else
		fc_buf = (StringInfo) PG_GETARG_POINTER(0);
	if (PG_ARGISNULL(1))
		PG_RETURN_NULL();
	fc_domainType = PG_GETARG_OID(1);

	/*
	 * 我们安排在一系列调用中仅查找所需的信息一次，
	 * 假设域类型在我们面前不会变化（这实际上不应该发生，但如果发生，就处理它）。
	 */
	fc_my_extra = (DomainIOData *) fcinfo->flinfo->fn_extra;
	if (fc_my_extra == NULL || fc_my_extra->domain_type != fc_domainType)
	{
		fc_my_extra = fc_domain_state_setup(fc_domainType, true,
									  fcinfo->flinfo->fn_mcxt);
		fcinfo->flinfo->fn_extra = (void *) fc_my_extra;
	}

	/*
	 * 调用基础类型的 typreceive 过程来转换数据。
	 */
	fc_value = ReceiveFunctionCall(&fc_my_extra->proc,
								fc_buf,
								fc_my_extra->typioparam,
								fc_my_extra->typtypmod);

	/*
	 * 进行必要的检查以确保它是一个有效的域值。
	 */
	fc_domain_check_input(fc_value, (fc_buf == NULL), fc_my_extra);

	if (fc_buf == NULL)
		PG_RETURN_NULL();
	else
		PG_RETURN_DATUM(fc_value);
}

/*
 * domain_check - 检查一个数据是否满足域的约束。
 * 如果 extra 和 mcxt 可以从例如 FmgrInfo 结构中获取，
 * 则可以传递它们，或者可以是 NULL，在这种情况下，
 * 每次调用重复设置。
 */
void domain_check(Datum fc_value, bool fc_isnull, Oid fc_domainType,
			 void **fc_extra, MemoryContext fc_mcxt)
{
	DomainIOData *fc_my_extra = NULL;

	if (fc_mcxt == NULL)
		fc_mcxt = CurrentMemoryContext;

	/*
	 * 我们安排在一系列调用中仅查找所需的信息一次，
	 * 假设域类型在我们面前不会变化（这实际上不应该发生，但如果发生，就处理它）。
	 */
	if (fc_extra)
		fc_my_extra = (DomainIOData *) *fc_extra;
	if (fc_my_extra == NULL || fc_my_extra->domain_type != fc_domainType)
	{
		fc_my_extra = fc_domain_state_setup(fc_domainType, true, fc_mcxt);
		if (fc_extra)
			*fc_extra = (void *) fc_my_extra;
	}

	/*
	 * 进行必要的检查以确保它是一个有效的域值。
	 */
	fc_domain_check_input(fc_value, fc_isnull, fc_my_extra);
}

/*
 * errdatatype --- 存储当前 errordata 中一个数据类型的 schema_name 和 datatype_name。
 */
int errdatatype(Oid fc_datatypeOid)
{
	HeapTuple	fc_tup;
	Form_pg_type fc_typtup;

	fc_tup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(fc_datatypeOid));
	if (!HeapTupleIsValid(fc_tup))
		elog(ERROR, "cache lookup failed for type %u", fc_datatypeOid);
	fc_typtup = (Form_pg_type) GETSTRUCT(fc_tup);

	err_generic_string(PG_DIAG_SCHEMA_NAME,
					   get_namespace_name(fc_typtup->typnamespace));
	err_generic_string(PG_DIAG_DATATYPE_NAME, NameStr(fc_typtup->typname));

	ReleaseSysCache(fc_tup);

	return 0;					/* 返回值无关紧要 */
}

/*
 * errdomainconstraint --- 存储当前 errordata 中一个域相关约束的 schema_name、datatype_name 和 constraint_name。
 */
int errdomainconstraint(Oid fc_datatypeOid, const char *fc_conname)
{
	errdatatype(fc_datatypeOid);
	err_generic_string(PG_DIAG_CONSTRAINT_NAME, fc_conname);

	return 0;					/* 返回值无关紧要 */
}
